本文从整体结构、缓存逻辑、Producer sequence等方面对Fresco做一些源码分析,希望看完后大家能对fresco的使用和理解更有自信。
整体结构
把fresco拆分成展示层和图片加载层来理解。
展示层主要包含DraweeView、DraweeHolder、DraweeController和DraweeHierachy。四个模块的持有关系就是箭头的指向,主要维护View相关内容和多层Drawable。比如解析XML中相关配置属性、根据图片加载状态显示对应Drawable等。
图片展示层是由ImagePipeline发起,核心是一系列的producer。Producer的执行顺序就是下图中从上到下的顺序,即先读内存缓存、切换线程、图片解码、读编码缓存、发起网络等等。每个Producer内部都有自己的Consumer,用来接收下层Producer的处理数据并做当前层相应的工作。Consumer层次间的回调顺序则是和其Producer反向。
后面会分别对展示层、加载流程、Producer sequence、缓存架构等进行细节理解。
展示层
DraweeView:
继承自ImageView,是SimpleDraweeView的基类,主要是做为DraweeHierachy的展示层。持有DraweeHolder实例。
实现类GenericDraweeView在初始化过程中GenericDraweeView.inflateHierarchy()
将我们在XML文件配置的styleable inflate出来,比如placeholderImage、fadeDuration、viewAspectRatio等等,具体各种效果配置可以参考官网。styleable具体解析在GenericDraweeHierarchyInflater.updateBuilder()
。GenericDraweeHierarchyBuilder构建出的DraweeHierachy实例会传给DraweeHolder和DraweeController持有。
看下最常用的SimpleDraweeView.setImageURI()方法。
1 | SimpleDraweeView.java |
1 | DraweeView.java |
1 | DraweeHolder.java |
1 | GenericDraweeHierachy.java |
所以从上面的代码流程可以看出,是DraweeView -> DraweeHolder -> DraweeHierachy的调用关系。
DraweeHolder:
做为DraweeView和DraweeHierarchy的纽带,内部持有DraweeHierarchy和DraweeController的实例。
同时维护着attach/detach逻辑,以及控制发起、取消ImagePipeline图片请求过程(具体图片请求逻辑后面主流程代码会讲到)。比如,当DraweeView attachToWindow()时,会执行下面代码
1 | DraweeHolder.java |
1 | AbstractDraweeController.java |
DraweeHierarchy:
具体实现在GenericDraweeHierarchy,内部组合了多层Drawable。从下面变量名称就可以看到具体Drawable层级内容。细节内容可以看下GenericDraweeHierarchy这个类。
1 | GenericDraweeHierarchy.java |
DraweeController:
响应DraweeView,通过ImagePipeline向producer sequence发起图片加载流程,代码在上面介绍DraweeHolder时也提到了。同时根据各种状态来控制不同layer的Drawable的展示。内部持有DraweeHierachy实例。
下面展示一些根据图片加载状态,通过DraweeHierachy修改图层内容的示例。
1 | AbstractDraweeController.java |
再简单回顾下:
- DraweeView继承自ImageView,承载View的工作,持有DraweeHolder实例。
- DraweeHolder作为DraweeView和DraweeHierachy的纽带,持有DraweeController和DraweeHierachy实例。
- DraweeHierachy管理Drawable层级,提供各种状态的Drawable。
- DraweeController主要负责逻辑控制,并根据图片加载状态更新DraweeHierachy的Drawable显示内容。
具体的持有关系看下图,持有关系就是箭头的指向。
图片加载流程概述
上面描述了fresco和开发接触最多的view层内容,view层之下是图片加载实现层。其实官网中对图片加载流程已经描述的很详细,我们先看下文字流程,对图片加载逻辑有个直观感受,再看后面的代码流程。
Fresco 中设计有一个叫做 Image Pipeline 的模块。它负责从网络,从本地文件系统,本地资源加载图片。为了最大限度节省空间和CPU时间,它含有3级缓存设计(2级内存,1级磁盘),分别是Bitmap Memory Cache、Encoded Memory Cache、Disk Cache。
大致流程如下:
- 检查内存缓存,如有,返回
- 后台线程开始后续工作
- 检查是否在未解码内存缓存中。如有,解码,变换,返回,然后缓存到内存缓存中。
- 检查是否在磁盘缓存中,如果有,变换,返回。缓存到未解码缓存和内存缓存中。
- 从网络或者本地加载。加载完成后,解码,变换,返回。存到各个缓存中。
下图非常直观的展示了整个加载流程以及线程模块。
主体代码流程
有了上面的描述,我们跟下图片请求的主体代码流程,包括发起加载网络图片(http schema)、三层内存处理、最终使用网络请求图片。
从最常用的请求图片接口SimpleDraweeView.setImageUri()开始,忽略不重要的代码分支。代码执行顺序就是下面代码块从上到下的顺序。
1 | ------------- 初始化PipelineDraweeController ------------- |
整个流程就讲完了,虽然代码量很多,但是逻辑一环扣一环还是非常清晰的。再回顾一下主要流程:
- SimpleDraweeView.setImageUri()发起图片请求
- 初始化PipelineDraweeController,走到PipelineDraweeControllerBuilder.obtainController()
- 在2中PipelineDraweeController初始化方法中获取
Supplier<DataSource>
,Supplier接口的get()实现中使用ImagePipeline获取图片,调用ImagePipeline.fetchDecodedImage() - 第3步之后,会启动一系列Producer过程,即调用Producer.produceResults()
- 先从内存缓存BitmapMemoryCacheGetProducer获取图片
- 内存缓存没有命中后,从编码内存EncodedMemoryCacheProducer中获取图片,这中间包括线程切换、图片resize和rotate、jpe渐进解码、获取图片metadata等逻辑封装
- 编码内存没有命中,从磁盘DiskCacheReadProducer获取图片
- 磁盘缓存没有命中,从网络获取图片。网络库默认走HTTPURLConnection,如果配置了OKHTTP,则走OkHttpNetworkFetcher。
Producer/Consumer
在主体代码流程中就能看到,从三级缓存/网络获取图片以及对上层的回调逻辑中,用到了大量的Producer/Consumer。官网中介绍ImagePipeline的工作流程,其本质就是producer sequence实现的。
Producer: 在image pipeline流程中构建一个处理图片的block。可以处理例如从三级缓存/网络中请求图片、解码、图片转换等。每一个Producer代表一个单独的图片处理任务。返回值类型由其泛型决定。
1 | /** |
Consumer:主要作为对应每一层Producer产生数据的消费方和相关状态回调。
1 | /** |
Producer责任链逻辑
通过下面例子简单讲下链式Producer和Consumer如何处理数据流的。
从上到下的链式producer中,从上到下依次是ProducerA/ConsumerA、ProducerB/ConsumerB、ProducerC/ConsumerC。
1 | class ProducerB { |
简单来说就是,每层Producer都会持有上层Consumer实例和下层Producer实例。本层能处理或不需要下层Producer处理时,通过上层Consumer实例向上返回,否则通过下层Producer实例往下走。即责任链的设计模式。
常用Producer解释
部分Producer的作用已经在主体代码流程中提到了,再具体讲下常用的一些Producer的作用。
1 | BitmapMemoryCacheProducer: |
构造Producer流链式结构
Producer流的入口在ImagePipeline.fetchDecodedImage()
1 | public DataSource<CloseableReference<CloseableImage>> fetchDecodedImage( |
Producer的创建都在ProducerSequenceFactory。每创建一个Producer时,都会优先创建其下层Producer作为构造参数,从而形成从上到下的链式结构。
1 | ProducerSequenceFactory.java |
自定义PostProcessor
业务开发中,如果我们想配置一个自定义的图片后处理器,比如对图片做高斯模糊、添加logo等特殊效果,可以通过ImageRequestBuilder.setPostprocessor()配置一个PostProcessor的Producer,这是最上层的Producer,从内存缓存中获取图片后会执行到这个Producer。
代码调用流程,在ImagePipeline.submitFetchRequest()之前,先通过ProducerSequenceFactory.getDecodedImageProducerSequence()获取sequence最顶层的Producer。
1 | val imageRequest = ImageRequestBuilder |
缓存架构
内存缓存
内存缓存,即Bitmap Memory Cache,本质是BitmapMemoryCacheProducer实现的,看下代码:
1 | public class BitmapMemoryCacheProducer implements Producer<CloseableReference<CloseableImage>> { |
从内存缓存中取图片的实现逻辑在CountingMemoryCache。
1 | CountingMemoryCache.java |
从mCachedEntries中获取到缓存后,用CloseableReference做了一层封装,主要是为了管理Bitmap内存,设计逻辑不复杂,但挺有意思,这一块后面单独会讲。
看下mCachedEntries.get(key)
,其实是一个LRUCache。
1 | CountingLruMap.java |
大家都知道fresco内存缓存使用的LruCache策略,到这里,也就有了解释。
编码内存缓存
编码内存缓存(Encoded Memory Cache)的逻辑和Bitmap Memory Cache类似,区别只是返回的图片包装类型是CloseableReference
1 | EncodedMemoryCacheProducer.java |
磁盘缓存
获取磁盘缓存(Disk Cache)的实现在DiskCacheReadProducer中。
1 | DiskCacheReadProducer.java |
BufferdDiskCache的读操作中,优先从disk cache的staging area中读取,没读到,切换到background thread,从磁盘里读:
1 | BufferdDiskCache.java |
看下mFileCache.getResource(key)
:
1 | DiskStorageCache.java |
看下获取resourceId的过程CacheKeyUtil.getResourceIds(key)
:
1 | CacheKeyUtil.java |
1 | SecureHashUtil.java |
所以,整体转换逻辑是将CacheKey先utf-8编码,然后在SHA-1加密,再base64编码。
磁盘读取文件Storage.getResource(resourceId, key)
内部实现主要是组装文件绝对路径的过程,感兴趣可以看下DynamicDefaultDiskStorage.get()
的实现,这里就不展开了。
以下面一个磁盘文件绝对路径名示例讲下整个路径的组成结构:
/data/user/0/your_package_name/cache/image_cache/v2.ols100.1/58/YvKmnI_toMgXiCXuQ5XdkEQDv7A.cnt
/data/user/0/your_package_name/cache: 应用的内部存储空间cache目录
image_cache:DiskCacheConfig配置的mBaseDirectoryName
v2.ols100.1: VersionSubdirectoryName
58:根据分区取得subdirectory
YvKmnI_toMgXiCXuQ5XdkEQDv7A:resourceId
.cnt:content文件类型后缀
自定义Fresco配置
下面提供一段fresco自定义配置,平时开发中可能并不需要那么多自定义项,仅做参考。
1 | // 大图磁盘缓冲区配置 |
可关闭的引用 ClosableReference
在内存缓存部分,从CountingMemoryCache获取到图片后使用CloseableReference进行一层包装,目的是在View不可用状态下,比如切到后台时(View detachFromWindow)释放图片,降低APP内存占用率。从LowMemoryKiller的角度考虑fresco的这种做法,也可以尽量减少当前APP非前台情况下被系统回收的概率。
但是这种做法也有一些缺陷,比如App切到后台,View会detachFromWindow,导致Drawable释放,等APP切回前台时,需要重新attach加载Drawable,能够看到View闪烁的不和谐情况。
官网对ClosableReference也做了一些解释,可以去看看。
ClosableReference内部使用引用计数的方式记录活跃的引用数,当引用数降为0时,ClosableReference就会释放持有的资源。下面通过CountingMemoryCache中使用CloseableReference对缓存图片包装的代码看下引用和释放的逻辑:
1 | CountingMemoryCache.java |
CloseableReference.of()入参有真正持有的对象t和ResourceReleaser(后面会提到)。
1 | CloseableReference.java |
CloseableReference构造方法中实例化了一个SharedReference,这是fresco自己的类,不同于android.content.SharedPreferences。
1 | private CloseableReference(T t, ResourceReleaser<T> resourceReleaser) { |
SharedReference构造方法中引用计数mRefCount置为1。对对象多一次持有,引用计数便会加1(addaddReference)。减少持有时(decreaseRefCount()),引用计数便会减1,当引用计数为0时,即decreaseRefCount() == 0
,引用对象便会释放mResourceReleaser.release(deleted)
,其中mResourceReleaser是CloseableReference.of()第二个入参。
1 | SharedReference.java |
前面提到,App切到后台时,View会执行onDetachFromWindow(),fresco会释放图片,看下具体代码。
1 | DraweeHolder.java |
看下Controller的onDetach(),
1 | AbstractDraweeController.java |
mDeferredReleaser.scheduleDeferredRelease()最终会执行到下面方法,其中mFetchedImage就是从Producer中获取到的图片。releaseImage(mFetchedImage)
内部使用CloseableReference减少引用数,当引用数为0时,释放资源。
1 | AbstractDraweeController.java |
1 | PipelineDraweeController.java |
1 | CloseableReference.java |
回顾一下:
图片的内存缓存使用CloseableReference为的是更好的管理Bitmap的内存占用,当View不可用时及时释放Bitmap内存,毕竟APP中Bitmap占用的内存才是大户。实现逻辑在SharedReference,内部采用活动引用计数的方式判断对象是否可以释放。